/*
 * Created on 1 Nov 2006
 * Created by Paul Gardner
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 */

package com.aelitis.azureus.core.download;

import java.util.List;

import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.config.ParameterListener;
import org.gudy.azureus2.core3.config.impl.TransferSpeedValidator;
import org.gudy.azureus2.core3.disk.DiskManager;
import org.gudy.azureus2.core3.disk.DiskManagerFileInfo;
import org.gudy.azureus2.core3.disk.DiskManagerPiece;
import org.gudy.azureus2.core3.download.DownloadManager;
import org.gudy.azureus2.core3.download.DownloadManagerListener;
import org.gudy.azureus2.core3.download.DownloadManagerPeerListener;
import org.gudy.azureus2.core3.download.impl.DownloadManagerAdapter;
import org.gudy.azureus2.core3.global.GlobalManager;
import org.gudy.azureus2.core3.peer.PEPeer;
import org.gudy.azureus2.core3.peer.PEPeerManager;
import org.gudy.azureus2.core3.torrent.TOTorrent;
import org.gudy.azureus2.core3.util.AEDiagnostics;
import org.gudy.azureus2.core3.util.AEDiagnosticsLogger;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.DisplayFormatters;
import org.gudy.azureus2.core3.util.RealTimeInfo;
import org.gudy.azureus2.core3.util.SystemTime;

import com.aelitis.azureus.core.peermanager.piecepicker.PiecePicker;
import com.aelitis.azureus.core.peermanager.piecepicker.PieceRTAProvider;
import com.aelitis.azureus.core.torrent.PlatformTorrentUtils;
import com.aelitis.azureus.core.util.average.Average;
import com.aelitis.azureus.core.util.average.AverageFactory;
import com.aelitis.azureus.util.ConstantsVuze;

public class EnhancedDownloadManager {
    public static int DEFAULT_MINIMUM_INITIAL_BUFFER_SECS_FOR_ETA = 30;

    // number of seconds of buffer required before we fall back to normal bt mode

    public static int MINIMUM_INITIAL_BUFFER_SECS;

    static {
        COConfigurationManager.addAndFireParameterListeners(new String[] { "filechannel.rt.buffer.millis" }, new ParameterListener() {
            public void parameterChanged(String parameterName) {
                int channel_buffer_millis = COConfigurationManager.getIntParameter("filechannel.rt.buffer.millis");

                MINIMUM_INITIAL_BUFFER_SECS = (2 * channel_buffer_millis) / 1000;
            }
        });
    }

    public static final int REACTIVATE_PROVIDER_PERIOD = 5 * 1000;
    public static final int REACTIVATE_PROVIDER_PERIOD_TICKS = REACTIVATE_PROVIDER_PERIOD / DownloadManagerEnhancer.TICK_PERIOD;

    public static final int LOG_PROG_STATS_PERIOD = 10 * 1000;
    public static final int LOG_PROG_STATS_TICKS = LOG_PROG_STATS_PERIOD / DownloadManagerEnhancer.TICK_PERIOD;

    private static final int content_stream_bps_min_increase_ratio = 5;
    private static final int content_stream_bps_max_increase_ratio = 8;

    private DownloadManagerEnhancer enhancer;
    private DownloadManager download_manager;

    private boolean explicit_progressive;

    private volatile PiecePicker current_piece_pickler;

    private volatile boolean progressive_active = false;

    private long content_min_delivery_bps;

    private int minimum_initial_buffer_secs_for_eta;

    private bufferETAProvider buffer_provider = new bufferETAProvider();

    private progressiveStats progressive_stats;

    private boolean marked_active;
    private boolean destroyed;

    private DownloadManagerListener dmListener;

    private EnhancedDownloadManagerFile[] enhanced_files;

    protected EnhancedDownloadManager(DownloadManagerEnhancer _enhancer, DownloadManager _download_manager) {
        enhancer = _enhancer;
        download_manager = _download_manager;

        DiskManagerFileInfo[] files = download_manager.getDiskManagerFileInfo();

        minimum_initial_buffer_secs_for_eta = DEFAULT_MINIMUM_INITIAL_BUFFER_SECS_FOR_ETA;

        enhanced_files = new EnhancedDownloadManagerFile[files.length];

        long offset = 0;

        for (int i = 0; i < files.length; i++) {

            DiskManagerFileInfo f = files[i];

            enhanced_files[i] = new EnhancedDownloadManagerFile(f, offset);

            offset += f.getLength();
        }

        int primary_index = getPrimaryFileIndex();

        EnhancedDownloadManagerFile primary_file = enhanced_files[primary_index == -1 ? 0 : primary_index];

        progressive_stats = createProgressiveStats(download_manager, primary_file);

        download_manager.addPeerListener(new DownloadManagerPeerListener() {
            public void peerManagerWillBeAdded(PEPeerManager peer_manager) {
            }

            public void peerManagerAdded(PEPeerManager manager) {
                synchronized (EnhancedDownloadManager.this) {

                    current_piece_pickler = manager.getPiecePicker();

                    if (progressive_active && current_piece_pickler != null) {

                        buffer_provider.activate(current_piece_pickler);
                    }
                }
            }

            public void peerManagerRemoved(PEPeerManager manager) {
                synchronized (EnhancedDownloadManager.this) {

                    if (progressive_active) {

                        setProgressiveMode(false);
                    }
                }
            }

            public void peerAdded(PEPeer peer) {
            }

            public void peerRemoved(PEPeer peer) {
            }
        });
    }

    public int getPrimaryFileIndex() {
        DiskManagerFileInfo info = download_manager.getDownloadState().getPrimaryFile();

        if (info == null) {
            return (-1);
        }
        return (info.getIndex());
    }

    public void setExplicitProgressive(int min_initial_buffer_secs, long min_bps, int file_index) {
        if (file_index >= 0 && file_index < enhanced_files.length) {

            explicit_progressive = true;

            minimum_initial_buffer_secs_for_eta = min_initial_buffer_secs;

            content_min_delivery_bps = min_bps;

            EnhancedDownloadManagerFile primary_file = enhanced_files[file_index];

            progressive_stats = createProgressiveStats(download_manager, primary_file);
        }
    }

    public String getName() {
        return (download_manager.getDisplayName());
    }

    public byte[] getHash() {
        TOTorrent t = download_manager.getTorrent();

        if (t == null) {

            return (null);
        }

        try {

            return (t.getHash());

        } catch (Throwable e) {

            return (null);
        }
    }

    public boolean isPlatform() {
        TOTorrent torrent = download_manager.getTorrent();

        if (torrent != null) {

            return (PlatformTorrentUtils.isContent(torrent, true));
        }

        return (false);
    }

    public EnhancedDownloadManagerFile[] getFiles() {
        return (enhanced_files);
    }

    protected long getTargetSpeed() {
        long target_speed = progressive_active ? progressive_stats.getStreamBytesPerSecondMax() : content_min_delivery_bps;

        if (target_speed < content_min_delivery_bps) {

            target_speed = content_min_delivery_bps;
        }

        return (target_speed);
    }

    protected boolean updateStats(int tick_count) {
        return (updateProgressiveStats(tick_count));
    }

    public boolean supportsProgressiveMode() {
        TOTorrent torrent = download_manager.getTorrent();

        if (torrent == null) {

            return (false);
        }

        return (enhancer.isProgressiveAvailable() && (PlatformTorrentUtils.isContentProgressive(torrent) || explicit_progressive));
    }

    public void prepareForProgressiveMode(boolean active) {
        enhancer.prepareForProgressiveMode(download_manager, active);
    }

    public boolean setProgressiveMode(boolean active) {
        return (setProgressiveMode(active, false));
    }

    protected boolean setProgressiveMode(boolean active, boolean switching_progressive_downloads) {
        TOTorrent torrent = download_manager.getTorrent();

        DiskManagerFileInfo primaryFile = download_manager.getDownloadState().getPrimaryFile();

        if (torrent == null || primaryFile == null) {

            return (false);
        }

        EnhancedDownloadManagerFile enhanced_file = enhanced_files[primaryFile.getIndex()];

        synchronized (this) {

            if (progressive_active == active) {

                return (true);
            }

            if (active && !supportsProgressiveMode()) {

                Debug.out("Attempt to set progress mode on non-progressible content - " + getName());

                return (false);
            }

            log("Progressive mode changed to " + active);

            final GlobalManager gm = download_manager.getGlobalManager();
            if (active) {
                if (dmListener == null) {
                    dmListener = new DownloadManagerAdapter() {
                        public void downloadComplete(DownloadManager manager) {
                            enhancer.resume();
                        }
                    };
                }
                download_manager.addListener(dmListener);

                // Check existing downloading torrents and turn off any
                // existing progressive/downloading
                Object[] dms = gm.getDownloadManagers().toArray();
                for (int i = 0; i < dms.length; i++) {
                    DownloadManager dmCheck = (DownloadManager) dms[i];
                    if (dmCheck.equals(download_manager)) {
                        continue;
                    }

                    if (!dmCheck.isDownloadComplete(false)) {
                        int state = dmCheck.getState();
                        if (state == DownloadManager.STATE_DOWNLOADING || state == DownloadManager.STATE_QUEUED) {
                            enhancer.pause(dmCheck);
                        }
                        EnhancedDownloadManager edmCheck = enhancer.getEnhancedDownload(dmCheck);
                        if (edmCheck != null && edmCheck.getProgressiveMode()) {
                            edmCheck.setProgressiveMode(false, true);
                        }
                    }
                }
                if (download_manager.isPaused()) {
                    enhancer.resume(download_manager);
                }

                // Make sure download can start by moving out of stop state
                // and putting at top
                if (download_manager.getState() == DownloadManager.STATE_STOPPED) {
                    download_manager.setStateWaiting();
                }

                if (download_manager.getPosition() != 1) {
                    download_manager.getGlobalManager().moveTo(download_manager, 1);
                }
            } else {
                download_manager.removeListener(dmListener);
                if (!switching_progressive_downloads) {
                    enhancer.resume();
                }
            }

            progressive_active = active;

            if (progressive_active) {

                enhancer.progressiveActivated();
            }

            if (current_piece_pickler != null) {

                if (progressive_active) {

                    buffer_provider.activate(current_piece_pickler);

                    progressive_stats.update(0);

                } else {

                    buffer_provider.deactivate(current_piece_pickler);

                    progressive_stats = createProgressiveStats(download_manager, enhanced_file);
                }
            } else {

                progressive_stats = createProgressiveStats(download_manager, enhanced_file);
            }

            if (!switching_progressive_downloads) {

                if (active) {

                    RealTimeInfo.setProgressiveActive(progressive_stats.getStreamBytesPerSecondMax());

                } else {

                    RealTimeInfo.setProgressiveInactive();
                }
            }
        }

        return (true);
    }

    public boolean getProgressiveMode() {
        return (progressive_active);
    }

    public long getProgressivePlayETA() {
        progressiveStats stats = getProgressiveStats();

        if (stats == null) {

            return (Long.MAX_VALUE);
        }

        long eta = stats.getETA();

        return (eta);
    }

    protected progressiveStats getProgressiveStats() {
        synchronized (this) {

            if (progressive_stats == null) {

                return (null);
            }

            return (progressive_stats.getCopy());
        }
    }

    protected progressiveStats createProgressiveStats(DownloadManager dm, EnhancedDownloadManagerFile file) {
        return (new progressiveStatsCommon(dm, file));
    }

    protected boolean updateProgressiveStats(int tick_count) {
        if (!progressive_active) {

            return (false);
        }

        synchronized (this) {

            if (!progressive_active || progressive_stats == null) {

                return (false);
            }

            if (tick_count % REACTIVATE_PROVIDER_PERIOD_TICKS == 0) {

                PiecePicker piece_picker = current_piece_pickler;

                if (piece_picker != null) {

                    buffer_provider.checkActivation(piece_picker);
                }
            }

            progressive_stats.update(tick_count);

            long current_max = progressive_stats.getStreamBytesPerSecondMax();

            if (RealTimeInfo.getProgressiveActiveBytesPerSec() != current_max) {

                RealTimeInfo.setProgressiveActive(current_max);
            }
        }

        return (true);
    }

    protected void setRTA(boolean active) {
        synchronized (this) {

            if (marked_active && !active) {

                marked_active = false;

                RealTimeInfo.removeRealTimeTask();
            }

            if (destroyed) {

                return;
            }

            if (!marked_active && active) {

                marked_active = true;

                RealTimeInfo.addRealTimeTask();
            }
        }
    }

    public long getContiguousAvailableBytes(int file_index, long file_start_offset, long stop_counting_after) {
        if (file_index < 0 || file_index >= enhanced_files.length) {

            return (-1);
        }

        EnhancedDownloadManagerFile efile = enhanced_files[file_index];

        DiskManagerFileInfo file = efile.getFile();

        DiskManager dm = download_manager.getDiskManager();

        if (dm == null) {

            if (file.getDownloaded() == file.getLength()) {

                return (file.getLength() - file_start_offset);
            }

            return (-1);
        }

        int piece_size = dm.getPieceLength();

        long start_index = efile.getByteOffestInTorrent() + file_start_offset;

        int first_piece_index = (int) (start_index / piece_size);
        int first_piece_offset = (int) (start_index % piece_size);
        int last_piece_index = file.getLastPieceNumber();

        DiskManagerPiece[] pieces = dm.getPieces();

        DiskManagerPiece first_piece = pieces[first_piece_index];

        long available = 0;

        if (!first_piece.isDone()) {

            boolean[] blocks = first_piece.getWritten();

            if (blocks == null) {

                if (first_piece.isDone()) {

                    available = first_piece.getLength() - first_piece_offset;
                }
            } else {

                int piece_offset = 0;

                for (int j = 0; j < blocks.length; j++) {

                    if (blocks[j]) {

                        int block_size = first_piece.getBlockSize(j);

                        piece_offset = piece_offset + block_size;

                        if (available == 0) {

                            if (piece_offset > first_piece_offset) {

                                available = piece_offset - first_piece_offset;
                            }
                        } else {

                            available += block_size;
                        }
                    } else {

                        break;
                    }
                }
            }
        } else {

            available = first_piece.getLength() - first_piece_offset;

            for (int i = first_piece_index + 1; i <= last_piece_index; i++) {

                if (stop_counting_after > 0 && available >= stop_counting_after) {

                    break;
                }

                DiskManagerPiece piece = pieces[i];

                if (piece.isDone()) {

                    available += piece.getLength();

                } else {

                    boolean[] blocks = piece.getWritten();

                    if (blocks == null) {

                        if (piece.isDone()) {

                            available += piece.getLength();

                        } else {

                            break;
                        }
                    } else {

                        for (int j = 0; j < blocks.length; j++) {

                            if (blocks[j]) {

                                available += piece.getBlockSize(j);

                            } else {

                                break;
                            }
                        }
                    }

                    break;
                }
            }
        }

        long max_available = file.getLength() - file_start_offset;

        if (available > max_available) {

            available = max_available;
        }

        return (available);
    }

    public DownloadManager getDownloadManager() {
        return download_manager;
    }

    protected void destroy() {
        synchronized (this) {

            setRTA(false);

            destroyed = true;
        }
    }

    protected void log(String str) {
        log(str, true);
    }

    protected void log(String str, boolean to_file) {
        log(download_manager, str, to_file);
    }

    protected void log(DownloadManager dm, String str, boolean to_file) {
        str = dm.toString() + ": " + str;

        if (to_file) {

            AEDiagnosticsLogger diag_logger = AEDiagnostics.getLogger("v3.Stream");

            diag_logger.log(str);
        }

        if (ConstantsVuze.DIAG_TO_STDOUT) {

            System.out.println(Thread.currentThread().getName() + "|" + System.currentTimeMillis() + "] " + str);
        }
    }

    protected class bufferETAProvider implements PieceRTAProvider {
        private boolean is_buffering = true;

        private long[] piece_rtas;

        private long last_buffer_size;
        private long last_buffer_size_time;

        private boolean active;
        private long last_recalc;

        protected void activate(PiecePicker picker) {
            synchronized (EnhancedDownloadManager.this) {

                if (!active) {

                    log("Activating RTA provider");

                    active = true;

                    picker.addRTAProvider(this);
                }
            }
        }

        protected void deactivate(PiecePicker picker) {
            synchronized (EnhancedDownloadManager.this) {

                if (active) {

                    log("Deactivating RTA provider");

                    picker.removeRTAProvider(this);
                }

                piece_rtas = null;

                active = false;
            }
        }

        protected void checkActivation(PiecePicker picker) {
            // might need to re-enable the buffer provider if speeds change

            if (getProgressivePlayETA() > 0) {

                synchronized (EnhancedDownloadManager.this) {

                    if (piece_rtas == null) {

                        activate(picker);
                    }
                }
            }
        }

        public long[] updateRTAs(PiecePicker picker) {
            long mono_now = SystemTime.getMonotonousTime();

            if (mono_now - last_recalc < 500) {

                return (piece_rtas);
            }

            last_recalc = mono_now;

            DiskManager disk_manager = download_manager.getDiskManager();

            progressiveStats stats = progressive_stats;

            if (disk_manager == null || stats == null || stats.getFile().isComplete()) {

                deactivate(picker);

                return (null);
            }

            EnhancedDownloadManagerFile file = stats.getFile();

            long abs_provider_pos = stats.getCurrentProviderPosition(true);
            long rel_provider_pos = stats.getCurrentProviderPosition(false);

            long buffer_bytes = stats.getBufferBytes();

            boolean buffering = getProgressivePlayETA() >= 0;

            if (buffering) {

                long buffer_size = getContiguousAvailableBytes(file.getIndex(), rel_provider_pos, buffer_bytes);

                if (buffer_size == buffer_bytes) {

                    buffering = false;
                }
            }

            if (buffering != is_buffering) {

                if (buffering) {

                    log("Switching to buffer mode");

                } else {

                    log("Switching to RTA mode");
                }

                is_buffering = buffering;
            }

            long piece_size = disk_manager.getPieceLength();

            int start_piece = (int) (abs_provider_pos / piece_size);

            int end_piece = file.getFile().getLastPieceNumber();

            piece_rtas = new long[picker.getNumberOfPieces()];

            long now = SystemTime.getCurrentTime();

            if (is_buffering) {

                for (int i = start_piece; i <= end_piece; i++) {

                    // not bothered about times here but need to use large increments to ensure
                    // that pieces are picked in order even for slower peers

                    piece_rtas[i] = now + (i * 60000);
                }

                long buffer_size = getContiguousAvailableBytes(file.getIndex(), rel_provider_pos, 0);

                if (last_buffer_size != buffer_size) {

                    last_buffer_size = buffer_size;

                    last_buffer_size_time = now;

                } else {

                    if (now < last_buffer_size_time) {

                        last_buffer_size_time = now;

                    } else {

                        long stalled_for = now - last_buffer_size_time;

                        long dl_speed = progressive_stats.getDownloadBytesPerSecond();

                        if (dl_speed > 0) {

                            long block_time = (DiskManager.BLOCK_SIZE * 1000) / dl_speed;

                            if (stalled_for > Math.max(5000, 5 * block_time)) {

                                long target_rta = now + block_time;

                                int blocked_piece_index = (int) ((abs_provider_pos + buffer_size) / disk_manager.getPieceLength());

                                DiskManagerPiece[] pieces = disk_manager.getPieces();

                                if (blocked_piece_index < pieces.length) {

                                    if (pieces[blocked_piece_index].isDone()) {

                                        blocked_piece_index++;

                                        if (blocked_piece_index < pieces.length) {

                                            if (pieces[blocked_piece_index].isDone()) {

                                                blocked_piece_index = -1;
                                            }
                                        } else {

                                            blocked_piece_index = -1;
                                        }
                                    }
                                }

                                if (blocked_piece_index >= 0) {

                                    piece_rtas[blocked_piece_index] = target_rta;

                                    log("Buffer provider: reprioritising lagging piece " + blocked_piece_index + " with rta " + block_time);
                                }
                            }
                        }
                    }
                }
            } else {

                long bytes_offset = 0;

                long max_bps = stats.getStreamBytesPerSecondMax();

                for (int i = start_piece; i <= end_piece; i++) {

                    piece_rtas[i] = now + (1000 * (bytes_offset / max_bps));

                    bytes_offset += piece_size;

                    if (bytes_offset > buffer_bytes) {

                        break;
                    }
                }
            }

            return (piece_rtas);
        }

        public long getCurrentPosition() {
            return (0);
        }

        public long getStartTime() {
            return (0);
        }

        public long getStartPosition() {
            return (0);
        }

        public long getBlockingPosition() {
            return (0);
        }

        public void setBufferMillis(long millis, long delay_millis) {
        }

        public String getUserAgent() {
            return (null);
        }
    }

    protected abstract class progressiveStats implements Cloneable {
        protected abstract EnhancedDownloadManagerFile getFile();

        protected abstract boolean isProviderActive();

        protected abstract long getCurrentProviderPosition(boolean absolute);

        protected abstract long getStreamBytesPerSecondMax();

        protected abstract long getStreamBytesPerSecondMin();

        protected abstract long getDownloadBytesPerSecond();

        protected abstract long getETA();

        public abstract long getBufferBytes();

        protected abstract long getSecondsToDownload();

        protected abstract long getSecondsToWatch();

        protected abstract void update(int tick_count);

        protected progressiveStats getCopy() {
            try {
                return ((progressiveStats) clone());

            } catch (CloneNotSupportedException e) {

                Debug.printStackTrace(e);

                return (null);
            }
        }

        protected String formatBytes(long l) {
            return (DisplayFormatters.formatByteCountToKiBEtc(l));
        }

        protected String formatSpeed(long l) {
            return (DisplayFormatters.formatByteCountToKiBEtcPerSec(l));
        }

    }

    protected class progressiveStatsCommon extends progressiveStats {
        private EnhancedDownloadManagerFile primary_file;

        private PieceRTAProvider current_provider;
        private String current_user_agent;

        private long content_stream_bps_min;
        private long content_stream_bps_max;

        private Average capped_download_rate_average = AverageFactory.MovingImmediateAverage(10);
        private Average discard_rate_average = AverageFactory.MovingImmediateAverage(10);
        private long last_discard_bytes = download_manager.getStats().getDiscarded();

        private long actual_bytes_to_download;
        private long weighted_bytes_to_download; // gives less weight to incomplete pieces

        private long provider_life_secs;
        private long provider_initial_position;
        private long provider_byte_position;
        private long provider_last_byte_position = -1;
        private long provider_blocking_byte_position;
        private Average provider_speed_average = AverageFactory.MovingImmediateAverage(10);

        protected progressiveStatsCommon(DownloadManager _dm, EnhancedDownloadManagerFile _primary_file) {
            primary_file = _primary_file;

            TOTorrent torrent = download_manager.getTorrent();

            content_stream_bps_min = explicit_progressive ? content_min_delivery_bps : PlatformTorrentUtils.getContentStreamSpeedBps(torrent);

            if (content_stream_bps_min == 0) {

                // hack in some test values for torrents that don't have a bps in them yet

                long size = torrent.getSize();

                if (size < 200 * 1024 * 1024) {

                    content_stream_bps_min = 30 * 1024;

                } else if (size < 1000 * 1024 * 1024L) {

                    content_stream_bps_min = 200 * 1024;

                } else {

                    content_stream_bps_min = 400 * 1024;
                }
            }

            // bump it up by a bit to be conservative to deal with fluctuations, discards etc.

            content_stream_bps_min += content_stream_bps_min / content_stream_bps_min_increase_ratio;

            content_stream_bps_max = content_stream_bps_min + (content_stream_bps_min / content_stream_bps_max_increase_ratio);

            setRTA(false);

            log(download_manager, "content_stream_bps=" + getStreamBytesPerSecondMin() + ",primary=" + primary_file.getFile().getIndex(), true);
        }

        protected void updateCurrentProvider(PieceRTAProvider provider) {
            long file_start = primary_file.getByteOffestInTorrent();

            if (current_provider != provider || provider == null) {

                current_provider = provider;
                current_user_agent = null;

                provider_speed_average = AverageFactory.MovingImmediateAverage(10);

                if (current_provider == null) {

                    provider_life_secs = 0;
                    provider_initial_position = file_start;
                    provider_byte_position = file_start;
                    provider_blocking_byte_position = -1;
                    provider_last_byte_position = -1;

                } else {

                    provider_initial_position = Math.max(file_start, current_provider.getStartPosition());

                    provider_byte_position = provider_initial_position;
                    provider_last_byte_position = provider_initial_position;

                    provider_blocking_byte_position = current_provider.getBlockingPosition();

                    provider_life_secs = (SystemTime.getCurrentTime() - current_provider.getStartTime()) / 1000;

                    if (provider_life_secs < 0) {

                        provider_life_secs = 0;
                    }
                }

                setRTA(current_provider != null);

            } else {

                provider_life_secs++;

                if (current_user_agent == null) {

                    current_user_agent = current_provider.getUserAgent();

                    if (current_user_agent != null) {

                        log("Provider user agent = " + current_user_agent);
                    }
                }

                provider_byte_position = Math.max(file_start, current_provider.getCurrentPosition());
                provider_blocking_byte_position = current_provider.getBlockingPosition();

                long bytes_read = provider_byte_position - provider_last_byte_position;

                provider_speed_average.update(bytes_read);

                provider_last_byte_position = provider_byte_position;
            }
        }

        protected boolean isProviderActive() {
            return (current_provider != null);
        }

        protected long getInitialProviderPosition() {
            return (provider_initial_position);
        }

        protected long getCurrentProviderPosition(boolean absolute) {
            long res = provider_byte_position;

            if (absolute) {

                if (res == 0) {

                    res = primary_file.getByteOffestInTorrent();
                }
            } else {

                res -= primary_file.getByteOffestInTorrent();

                if (res < 0) {

                    res = 0;
                }
            }

            return (res);
        }

        protected long getProviderLifeSecs() {
            return (provider_life_secs);
        }

        protected void update(int tick_count) {
            long download_rate = download_manager.getStats().getDataReceiveRate();

            capped_download_rate_average.update(download_rate);

            long discards = download_manager.getStats().getDiscarded();

            discard_rate_average.update(discards - last_discard_bytes);

            last_discard_bytes = discards;

            DiskManager disk_manager = download_manager.getDiskManager();

            PiecePicker picker = current_piece_pickler;

            if (getStreamBytesPerSecondMin() > 0 && disk_manager != null && picker != null) {

                List providers = picker.getRTAProviders();

                long max_cp = 0;

                PieceRTAProvider best_provider = null;

                for (int i = 0; i < providers.size(); i++) {

                    PieceRTAProvider provider = (PieceRTAProvider) providers.get(i);

                    if (provider.getStartTime() > 0) {

                        long cp = provider.getCurrentPosition();

                        if (cp >= max_cp) {

                            best_provider = provider;

                            max_cp = cp;
                        }
                    }
                }

                updateCurrentProvider(best_provider);

                if (best_provider != null) {

                    // the file channel provider will try best-effort-RTA based which will result
                    // in high discard - back it off based on how much slack we have

                    long relative_pos = getCurrentProviderPosition(false);

                    long buffer_bytes = getContiguousAvailableBytes(primary_file.getIndex(), relative_pos, getStreamBytesPerSecondMin() * 60);

                    long buffer_secs = buffer_bytes / getStreamBytesPerSecondMin();

                    // don't be too aggresive with small buffers

                    buffer_secs = Math.max(10, buffer_secs);

                    best_provider.setBufferMillis(15 * 1000, buffer_secs * 1000);
                }

                DiskManagerPiece[] pieces = disk_manager.getPieces();

                actual_bytes_to_download = 0;
                weighted_bytes_to_download = 0;

                int first_incomplete_piece = -1;

                int piece_size = disk_manager.getPieceLength();

                int last_piece_number = primary_file.getFile().getLastPieceNumber();

                for (int i = (int) (provider_byte_position / piece_size); i <= last_piece_number; i++) {

                    DiskManagerPiece piece = pieces[i];

                    if (piece.isDone()) {

                        continue;
                    }

                    if (first_incomplete_piece == -1) {

                        first_incomplete_piece = i;
                    }

                    boolean[] blocks = piece.getWritten();

                    int bytes_this_piece = 0;

                    if (blocks == null) {

                        bytes_this_piece = piece.getLength();

                    } else {
                        for (int j = 0; j < blocks.length; j++) {

                            if (!blocks[j]) {

                                bytes_this_piece += piece.getBlockSize(j);
                            }
                        }
                    }

                    if (bytes_this_piece > 0) {

                        actual_bytes_to_download += bytes_this_piece;

                        int diff = i - first_incomplete_piece;

                        if (diff == 0) {

                            weighted_bytes_to_download += bytes_this_piece;

                        } else {

                            int weighted_bytes_done = piece.getLength() - bytes_this_piece;

                            weighted_bytes_done = (weighted_bytes_done * (pieces.length - i)) / (pieces.length - first_incomplete_piece);

                            weighted_bytes_to_download += piece.getLength() - weighted_bytes_done;
                        }
                    }
                }
            }

            log(getString(), tick_count % LOG_PROG_STATS_TICKS == 0);
        }

        protected long getETA() {
            DiskManagerFileInfo file = primary_file.getFile();

            if (file.getLength() == file.getDownloaded()) {

                return (0);
            }

            long download_rate = getDownloadBytesPerSecond();

            if (download_rate <= 0) {

                return (Long.MAX_VALUE);
            }

            long buffer_bytes = getBufferBytes();

            long buffer_done = getContiguousAvailableBytes(file.getIndex(), getCurrentProviderPosition(false), buffer_bytes);

            long rem_buffer = buffer_bytes - buffer_done; // ok as initial dl is forced in order byte buffer-rta

            long rem_secs = (rem_buffer <= 0) ? 0 : (rem_buffer / download_rate);

            long secs_to_download = getSecondsToDownload();

            long secs_to_watch = getSecondsToWatch();

            long eta = secs_to_download - secs_to_watch;

            if (rem_secs > eta && rem_secs > 0) {

                eta = rem_secs;
            }

            return (eta);
        }

        protected long getStreamBytesPerSecondMax() {
            return (content_stream_bps_max);
        }

        protected long getStreamBytesPerSecondMin() {
            return (content_stream_bps_min);
        }

        public long getBufferBytes() {
            long min_dl = minimum_initial_buffer_secs_for_eta * getStreamBytesPerSecondMax();

            return (min_dl);
        }

        protected EnhancedDownloadManagerFile getFile() {
            return (primary_file);
        }

        protected long getDownloadBytesPerSecond() {
            long original = (long) capped_download_rate_average.getAverage();

            long current = original;

            int dl_limit = download_manager.getStats().getDownloadRateLimitBytesPerSecond();

            if (dl_limit > 0) {

                current = Math.min(current, dl_limit);
            }

            int global_limit = TransferSpeedValidator.getGlobalDownloadRateLimitBytesPerSecond();

            if (global_limit > 0) {

                current = Math.min(current, global_limit);
            }

            return (current);
        }

        protected long getSecondsToDownload() {
            long download_rate = getDownloadBytesPerSecond();

            if (download_rate == 0) {

                return (Long.MAX_VALUE);
            }

            return (weighted_bytes_to_download / download_rate);
        }

        public long getSecondsToWatch() {
            return ((primary_file.getLength() - getCurrentProviderPosition(false)) / getStreamBytesPerSecondMin());
        }

        protected String getString() {
            long dl_rate = getDownloadBytesPerSecond();

            long buffer_bytes = getBufferBytes();

            long buffer_done = getContiguousAvailableBytes(primary_file.getIndex(), getCurrentProviderPosition(false), buffer_bytes);

            return ("play_eta=" + getETA() + "/d=" + getSecondsToDownload() + "/w=" + getSecondsToWatch() + ", dl_rate=" + formatSpeed(dl_rate)
                    + ", download_rem=" + formatBytes(weighted_bytes_to_download) + "/" + formatBytes(actual_bytes_to_download) + ", discard_rate="
                    + formatSpeed((long) discard_rate_average.getAverage()) + ", buffer: " + buffer_bytes + "/" + buffer_done + ", prov: byte="
                    + formatBytes(provider_byte_position) + " secs=" + (provider_byte_position / getStreamBytesPerSecondMin()) + " speed="
                    + formatSpeed((long) provider_speed_average.getAverage()) + " block= " + formatBytes(provider_blocking_byte_position));
        }
    }
}
